'# Copyright (c) 2008-2011 Hacker Wez
'# Source code distributed under the GNU Lesser license
'# http://bluepillredpill.codeplex.com/license
Imports SdlDotNet.Core
Imports SdlDotNet.Graphics
Module main

    Const GLYPH_VOLATILITY_MIN As Integer = 1       'vola min
    Const GLYPH_VOLATILITY_MAX As Integer = 100     'vola max
    Const VOLATILITY_RATIO As Integer = 10          'vola ratio: higher mean less volatile, ie less chance to change a glyph randomly
    Const GLYPH_SPEED_MIN As Integer = 4
    Const GLYPH_SPEED_MAX As Integer = 7
    Const GLYPH_DECAY_MAX As Integer = 7            ' (decay - glyph) slower glyphs decay quicker, faster ones leave longer trails - larger values of MAX decay quicker
    Const FONT_SIZE As Integer = 16

    Const GLYPHMAP_ITEMS As Integer = 48
    Const MAX_ONSCREEN_GLYPHS As Integer = 25
    Const MIN_GLYPHS_PER_SESSION As Integer = 1000  ' amt of glyphs to show (at the least) before we consider switching over to terminal mode again

    Dim rng As New Random
    Dim footprints As New List(Of footprint)        ' our glyph trails and things
    Dim glyphs As New List(Of glyph)

    Dim background As Surface                       ' all our sdl surfaces we use to enter the matrix
    Dim screen As Surface
    Dim glyphmap As Surface
    Dim charmap As Surface
    Dim textlayer As Surface
    Dim debug_info As Surface

    Dim screen_width As Integer
    Dim screen_height As Integer
    Dim windowed As Boolean = False                 ' toggle with --window commandline parameter
    Dim running As Boolean = False

    ' terminal stuff
    Const TERM_KEYPRESS_DELAY_MIN As Integer = 0   ' how fast we press keys
    Const TERM_KEYPRESS_DELAY_MAX As Integer = 2   ' how slow we press keys
    Const TERM_CHARS As String = " _.,:->#abcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*()"
    Const TERM_LINE_PAUSE As Integer = 30           ' wait before next line starts
    Const TERM_SWITCH_CHANCE As Integer = 5         ' percent chance to switch to terminal mode (only after MIN_GLYPHS_PER_SESSION is reached)

    Dim term_font As SdlDotNet.Graphics.Font
    Dim term_debug As Boolean = False
    Dim term_cmd As String = ""                     ' current terminal command
    Dim term_cmd_index As Integer = 0               ' current term cmd position
    Dim term_x_offset As Integer = 0                ' every crlf resets the x offset
    Dim term_y_offset As Integer = 0                ' every crlf adds to the y offset
    Dim term_commands() As String = { _
        ">Call trans opt: received 2-19-98 13:24:18 REC:Log>Trace program running>(312) 555 0690>" _
        , ">Call trans opt: received 9-18-99 14:32:21 REC:Log>WARNING: carrier anomaly>Trace program: running...<System Failure<" _
        , ">init shell>data port forwarding via secure tunnel>comms linked>" _
        , ">tx signal out :: rx data in :: linking pipes>" _
        , ">received secure message>piping decryptor chain>enter passphrase<**********<accepted. rotating salt keys< logic state machine error<" _
        , ">loading kernel...>building modules...>linking data nodes :: builing mesh map>anomaly found!<" _
        , ">loading rainbow table...< done>decrypting data strea...<done>error: data integrity failed :: data tampering detected!<" _
        , ">uplink established :: requesting xfer>host integrity compromised :: damaged link unmounted<" _
        , ">mounting secure data streams...<done>xfering data...<15K nodes...<217K nodes...<756K nodes tx>synching streams :: closing mountpoints<goodbye...<" _
        }

    Dim sleep_time As Integer = 0
    Dim glyphs_spawned As Integer = 0               ' number of glyphs spawned since last terminal text was shown


    Enum MACHINE_STATES
        STATE_TERMINAL  '# terminal text being displayed
        STATE_SIGTERM   '# glyphs wind-down, draw them, but don't spawn new ones
        STATE_ONLINE    '# inside the matrix (glyphs flowing down)
        STATE_SLEEP     '# we sleep for a little, 
    End Enum

    Dim machinestate As New Stack(Of MACHINE_STATES)


    Sub main()

        process_args()
        If Not running Then Exit Sub

        Try

            ' blank out non primary monitors
            If (Not windowed) Then
                For Each s As Screen In Windows.Forms.Screen.AllScreens
                    If Not s.Primary Then
                        Dim form As Form
                        form = New BlankScreen
                        form.StartPosition = FormStartPosition.Manual
                        form.FormBorderStyle = FormBorderStyle.None
                        form.BackColor = Color.Black
                        form.Left = s.Bounds.Left
                        form.Top = s.Bounds.Top
                        form.Width = s.Bounds.Width
                        form.Height = s.Bounds.Height
                        form.Show()
                    End If
                Next
            End If

            Dim modes() As Size = Video.ListModes()
            If modes.Length = 0 Then
                screen_width = Windows.Forms.Screen.PrimaryScreen.Bounds.Width
                screen_height = Windows.Forms.Screen.PrimaryScreen.Bounds.Height
            Else
                Array.Reverse(modes)
                screen_width = modes(0).Width
                screen_height = modes(0).Height
            End If

            If (windowed) Then
                screen_width = 640
                screen_height = 200
            End If

            background = New Surface(screen_width, screen_height)
            background.Blit(New Surface(My.Resources.scanline_monitor), New Rectangle(0, 0, screen_width, screen_height))
            glyphmap = New Surface(My.Resources.glyphmap)

            fill_glyphs()
            Set_Term_Command(0)
            buildcharmap(Color.Lime)
            reset_temrinal()
            machinestate.Push(MACHINE_STATES.STATE_ONLINE)
            machinestate.Push(MACHINE_STATES.STATE_TERMINAL)

            term_font = New SdlDotNet.Graphics.Font(My.Resources.bitwise, 12)
            screen = Video.SetVideoMode(screen_width, screen_height, False, False, Not windowed)
            SdlDotNet.Input.Mouse.ShowCursor = False

            Events.TargetFps = 30
            Video.WindowCaption = "Blue Pill, Red Pill"
            AddHandler Events.Tick, AddressOf Tick
            AddHandler Events.KeyboardDown, AddressOf Kbdown
            AddHandler Events.MouseButtonDown, AddressOf Mouseydown
            AddHandler Events.Quit, AddressOf Quit
            Events.Run()

        Catch ex As Exception
            Dim form As New ErrorScreen
            form.Text = ex.ToString
            Application.Run(form)
        End Try

    End Sub

    Sub fill_glyphs()
        For idx As Integer = 1 To MAX_ONSCREEN_GLYPHS
            glyphs.Add(create_glyph)
        Next
    End Sub

    Sub reset_temrinal()
        term_cmd_index = 0
        term_x_offset = 0
        term_y_offset = 0
        ' clear previous underscore
        textlayer.Erase(New Rectangle(0, 0, screen_width, screen_height), background)
    End Sub

    Sub buildcharmap(ByVal textcolor As Color)
        textlayer = New Surface(screen_width, screen_height)
        textlayer.Transparent = True
        charmap = New Surface((FONT_SIZE * 1) * TERM_CHARS.Length, FONT_SIZE * 2)
        charmap.Transparent = True
        Dim idx As Integer = 0
        Dim txt As New SdlDotNet.Graphics.Font(My.Resources.bitwise, FONT_SIZE)
        For Each ch As Char In TERM_CHARS
            charmap.Blit(txt.Render(ch, textcolor, True), New Point(idx * FONT_SIZE, 0))
            idx += 1
        Next
        txt.Close()
    End Sub

    Sub process_args()
        For Each arg As String In My.Application.CommandLineArgs
            If arg = "--window" Then windowed = True
            If arg = "/s" Then running = True
        Next
    End Sub

    Sub Quit(ByVal sender As Object, ByVal e As QuitEventArgs)
        term_font.Dispose()
        Events.QuitApplication()
    End Sub

    Sub Tick(ByVal sender As Object, ByVal e As TickEventArgs)
        screen.Erase(New Rectangle(0, 0, screen_width, screen_height), background)

        Dim istate As MACHINE_STATES = machinestate.Peek
        Select Case istate

            Case MACHINE_STATES.STATE_ONLINE, MACHINE_STATES.STATE_SIGTERM
                ' ## ONLINE

                ' footprints
                For idx As Integer = 0 To footprints.Count - 1
                    If (idx < footprints.Count) Then
                        Dim f As footprint = footprints(idx)
                        mutate_glyph(f)
                        If f.IsAlive Then
                            'blit
                            screen.Blit(glyphmap, f.ToDestPoint, f.ToBlurSourceRect)
                        Else
                            footprints.RemoveAt(idx)
                            idx -= 1    'rerun this last loop
                        End If
                    End If
                Next

                ' glyphs
                For idx As Integer = 0 To glyphs.Count - 1
                    If (idx < glyphs.Count) Then
                        Dim g As glyph = glyphs(idx)
                        mutate_glyph(g)
                        g.Y += g.Speed
                        ' blur
                        screen.Blit(glyphmap, g.ToDestPoint, g.ToBlurSourceRect)
                        ' glyph
                        screen.Blit(glyphmap, g.ToDestPoint, g.ToSourceRect)
                        'kill
                        If (g.Y > screen_height) Then
                            glyphs.RemoveAt(idx)
                            idx -= 1
                            'while still in the matrix
                            If (istate = MACHINE_STATES.STATE_ONLINE) Then
                                'born again
                                glyphs.Add(create_glyph)
                                ' to save cycles, only after a glyph dies do we now check if it's a good time to switch to terminal mode
                                If (glyphs_spawned > MIN_GLYPHS_PER_SESSION) Then
                                    ' if our random mark gets hit, switch on over to the console, neo
                                    If rng.Next(100) < TERM_SWITCH_CHANCE Then
                                        glyphs_spawned = 0
                                        ' push term state. note we are still under the 'if state=online' block, so this push cant be called twice by accident.
                                        machinestate.Push(MACHINE_STATES.STATE_SIGTERM)
                                    End If
                                End If
                                ' nope, not yet, but keep on counting how many we spawned anyway
                                glyphs_spawned += 1
                            End If
                        End If
                    End If
                Next

                ' sigterm is waiting for glyphs and trails to die, so we can push to the next state
                If (glyphs.Count = 0 And footprints.Count = 0) Then
                    If (istate = MACHINE_STATES.STATE_SIGTERM) Then
                        ' fill glyphs
                        fill_glyphs()
                        ' pop sigterm
                        machinestate.Pop()
                        ' push terminal
                        Set_Term_Command()
                        machinestate.Push(MACHINE_STATES.STATE_TERMINAL)
                        ' push sleep
                        sleep(200)
                    End If
                End If

                If (term_debug) Then debug_info = term_font.Render(String.Format("MACHINE_{0} # GLYPHS: {1} # SPAWNED: {2}/{3}", machinestate.Peek.ToString, glyphs.Count, glyphs_spawned, MIN_GLYPHS_PER_SESSION), Color.Yellow, Color.Blue)
                update_screen()

            Case MACHINE_STATES.STATE_SLEEP
                ' ## SLEEP
                sleep_time -= 1
                If sleep_time < 0 Then machinestate.Pop()

            Case MACHINE_STATES.STATE_TERMINAL
                ' ## TERMINAL

                If (term_debug) Then debug_info = term_font.Render(String.Format("MACHINE_{0} # CODE_POS: {1}/{2}", machinestate.Peek.ToString, term_cmd_index, term_cmd.Length), Color.Lime, Color.Blue)

                ' enter the matrix after commands are printed and pause is over
                If (term_cmd_index < term_cmd.Length) Then

                    ' clear previous underscore
                    textlayer.Erase(New Rectangle(term_x_offset + FONT_SIZE, term_y_offset, FONT_SIZE, FONT_SIZE * 2), background)

                    ' get the char and index in the charmap
                    Dim ch As Char = Char.ToLowerInvariant(term_cmd(term_cmd_index))
                    Dim char_idx As Integer = TERM_CHARS.IndexOf(ch)

                    ' update x/y offsets locations
                    If (ch = ">") Then
                        term_x_offset = 0
                        term_y_offset += FONT_SIZE
                        sleep(TERM_LINE_PAUSE)
                    ElseIf (ch = "<") Then
                        sleep(TERM_LINE_PAUSE)
                    Else
                        term_x_offset += FONT_SIZE
                        sleep(rng.Next(TERM_KEYPRESS_DELAY_MIN, TERM_KEYPRESS_DELAY_MAX))
                    End If

                    ' render char
                    textlayer.Blit(charmap, New Point(term_x_offset, term_y_offset), New Rectangle(char_idx * FONT_SIZE, 0, FONT_SIZE * 1, FONT_SIZE * 2))
                    ' render underscore (x-offset by font_size)
                    textlayer.Blit(charmap, New Point(term_x_offset + FONT_SIZE, term_y_offset), New Rectangle(TERM_CHARS.IndexOf("_"c) * FONT_SIZE, 0, FONT_SIZE * 1, FONT_SIZE * 2))

                    term_cmd_index += 1

                    ' render textlayer
                    screen.Blit(textlayer, New Point(0, 0))
                    update_screen()

                Else
                    ' reset terminal for next time
                    reset_temrinal()
                    ' pop terminal
                    machinestate.Pop()
                    ' push sleep
                    sleep(TERM_LINE_PAUSE)
                End If

        End Select

    End Sub

    Sub update_screen()
        ' render debug data
        If (term_debug) Then screen.Blit(debug_info)
        ' update terminal
        screen.Update()
    End Sub

    Sub sleep(ByVal ticks As Integer)
        sleep_time = ticks
        machinestate.Push(MACHINE_STATES.STATE_SLEEP)
    End Sub

    Sub Kbdown(ByVal sender As Object, ByVal e As SdlDotNet.Input.KeyboardEventArgs)
        Select Case e.Key
            Case SdlDotNet.Input.Key.T
                If machinestate.Peek = MACHINE_STATES.STATE_ONLINE Then
                    machinestate.Push(MACHINE_STATES.STATE_SIGTERM)
                End If
            Case SdlDotNet.Input.Key.P
                ' pop the stack
                If machinestate.Count > 1 Then machinestate.Pop()
            Case SdlDotNet.Input.Key.D
                debug_info = term_font.Render(String.Format("machine state:{0}\nglyphs:{1}\nspawned:{2}".Replace("\n", ControlChars.CrLf), machinestate.Peek.ToString, glyphs.Count, glyphs_spawned), Color.Yellow, Color.Blue)
                term_debug = term_debug Xor True
            Case Else
                Events.QuitApplication()
        End Select
    End Sub

    Sub Mouseydown(ByVal sender As Object, ByVal e As SdlDotNet.Input.MouseButtonEventArgs)
        Events.QuitApplication()
    End Sub

    Function create_glyph() As glyph
        Dim g As New glyph
        g.X = rng.Next(screen_width)
        g.Y = -rng.Next(screen_height) ' randomize the start-y a bit
        g.Speed = rng.Next(GLYPH_SPEED_MIN, GLYPH_SPEED_MAX)
        g.Volatility = rng.Next(GLYPH_VOLATILITY_MIN, GLYPH_VOLATILITY_MAX) \ VOLATILITY_RATIO
        g.Index = rng.Next(GLYPHMAP_ITEMS)
        g.Decay = GLYPH_DECAY_MAX - g.Speed
        Return g
    End Function

    ''' <summary>
    ''' potentially mutate the glyph into another one, and update the position if not a footprint
    ''' </summary>
    ''' <remarks></remarks>
    Sub mutate_glyph(ByVal g As glyphbase)

        Dim dice As Integer = 0
        dice = rng.Next(GLYPH_VOLATILITY_MIN, GLYPH_VOLATILITY_MAX)
        If (g.Volatility > dice) Then g.Index = rng.Next(GLYPHMAP_ITEMS)

        Dim item As Object = g.spawn_footprint
        If Not IsNothing(item) Then footprints.Add(CType(item, footprint))

    End Sub

    Sub Set_Term_Command(Optional ByVal index As Integer = -1)
        If (index = -1) Then
            term_cmd = term_commands(rng.Next(0, term_commands.Length))
        Else
            term_cmd = term_commands(index)
        End If
    End Sub



End Module
